 /*******************************************************************************
  * Copyright (c) 2000, 2007 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  *******************************************************************************/

 package org.eclipse.ui;

 import java.io.IOException ;
 import java.io.PrintWriter ;
 import java.io.Reader ;
 import java.io.Writer ;
 import java.util.ArrayList ;

 import javax.xml.parsers.DocumentBuilder ;
 import javax.xml.parsers.DocumentBuilderFactory ;
 import javax.xml.parsers.ParserConfigurationException ;

 import org.eclipse.ui.internal.WorkbenchMessages;
 import org.eclipse.ui.internal.WorkbenchPlugin;
 import org.w3c.dom.Attr ;
 import org.w3c.dom.Document ;
 import org.w3c.dom.Element ;
 import org.w3c.dom.NamedNodeMap ;
 import org.w3c.dom.Node ;
 import org.w3c.dom.NodeList ;
 import org.w3c.dom.Text ;
 import org.xml.sax.InputSource ;
 import org.xml.sax.SAXException ;

 /**
  * This class represents the default implementation of the
  * <code>IMemento</code> interface.
  * <p>
  * This class is not intended to be extended by clients.
  * </p>
  *
  * @see IMemento
  */
 public final class XMLMemento implements IMemento {
     private Document factory;

     private Element element;

     /**
      * Creates a <code>Document</code> from the <code>Reader</code>
      * and returns a memento on the first <code>Element</code> for reading
      * the document.
      * <p>
      * Same as calling createReadRoot(reader, null)
      * </p>
      *
      * @param reader the <code>Reader</code> used to create the memento's document
      * @return a memento on the first <code>Element</code> for reading the document
      * @throws WorkbenchException if IO problems, invalid format, or no element.
      */
     public static XMLMemento createReadRoot(Reader reader)
             throws WorkbenchException {
         return createReadRoot(reader, null);
     }

     /**
      * Creates a <code>Document</code> from the <code>Reader</code>
      * and returns a memento on the first <code>Element</code> for reading
      * the document.
      *
      * @param reader the <code>Reader</code> used to create the memento's document
      * @param baseDir the directory used to resolve relative file names
      * in the XML document. This directory must exist and include the
      * trailing separator. The directory format, including the separators,
      * must be valid for the platform. Can be <code>null</code> if not
      * needed.
      * @return a memento on the first <code>Element</code> for reading the document
      * @throws WorkbenchException if IO problems, invalid format, or no element.
      */
     public static XMLMemento createReadRoot(Reader reader, String baseDir)
             throws WorkbenchException {
         String errorMessage = null;
         Exception exception = null;

         try {
             DocumentBuilderFactory factory = DocumentBuilderFactory
                     .newInstance();
             DocumentBuilder parser = factory.newDocumentBuilder();
             InputSource source = new InputSource (reader);
             if (baseDir != null) {
                 source.setSystemId(baseDir);
             }
             Document document = parser.parse(source);
             NodeList list = document.getChildNodes();
             for (int i = 0; i < list.getLength(); i++) {
                 Node node = list.item(i);
                 if (node instanceof Element ) {
                     return new XMLMemento(document, (Element ) node);
                 }
             }
         } catch (ParserConfigurationException e) {
             exception = e;
             errorMessage = WorkbenchMessages.XMLMemento_parserConfigError;
         } catch (IOException e) {
             exception = e;
             errorMessage = WorkbenchMessages.XMLMemento_ioError;
         } catch (SAXException e) {
             exception = e;
             errorMessage = WorkbenchMessages.XMLMemento_formatError;
         }

         String problemText = null;
         if (exception != null) {
             problemText = exception.getMessage();
         }
         if (problemText == null || problemText.length() == 0) {
             problemText = errorMessage != null ? errorMessage
                     : WorkbenchMessages.XMLMemento_noElement;
         }
         throw new WorkbenchException(problemText, exception);
     }

     /**
      * Returns a root memento for writing a document.
      *
      * @param type the element node type to create on the document
      * @return the root memento for writing a document
      */
     public static XMLMemento createWriteRoot(String type) {
         Document document;
         try {
             document = DocumentBuilderFactory.newInstance()
                     .newDocumentBuilder().newDocument();
             Element element = document.createElement(type);
             document.appendChild(element);
             return new XMLMemento(document, element);
         } catch (ParserConfigurationException e) {
 // throw new Error(e);
 throw new Error (e.getMessage());
         }
     }

     /**
      * Creates a memento for the specified document and element.
      * <p>
      * Clients should use <code>createReadRoot</code> and
      * <code>createWriteRoot</code> to create the initial
      * memento on a document.
      * </p>
      *
      * @param document the document for the memento
      * @param element the element node for the memento
      */
     public XMLMemento(Document document, Element element) {
         super();
         this.factory = document;
         this.element = element;
     }

     /* (non-Javadoc)
      * Method declared in IMemento.
      */
     public IMemento createChild(String type) {
         Element child = factory.createElement(type);
         element.appendChild(child);
         return new XMLMemento(factory, child);
     }

     /* (non-Javadoc)
      * Method declared in IMemento.
      */
     public IMemento createChild(String type, String id) {
         Element child = factory.createElement(type);
         child.setAttribute(TAG_ID, id == null ? "" : id); //$NON-NLS-1$
 element.appendChild(child);
         return new XMLMemento(factory, child);
     }

     /* (non-Javadoc)
      * Method declared in IMemento.
      */
     public IMemento copyChild(IMemento child) {
         Element childElement = ((XMLMemento) child).element;
         Element newElement = (Element ) factory.importNode(childElement, true);
         element.appendChild(newElement);
         return new XMLMemento(factory, newElement);
     }

     /* (non-Javadoc)
      * Method declared in IMemento.
      */
     public IMemento getChild(String type) {

         // Get the nodes.
 NodeList nodes = element.getChildNodes();
         int size = nodes.getLength();
         if (size == 0) {
             return null;
         }

         // Find the first node which is a child of this node.
 for (int nX = 0; nX < size; nX++) {
             Node node = nodes.item(nX);
             if (node instanceof Element ) {
                 Element element = (Element ) node;
                 if (element.getNodeName().equals(type)) {
                     return new XMLMemento(factory, element);
                 }
             }
         }

         // A child was not found.
 return null;
     }

     /* (non-Javadoc)
      * Method declared in IMemento.
      */
     public IMemento[] getChildren(String type) {

         // Get the nodes.
 NodeList nodes = element.getChildNodes();
         int size = nodes.getLength();
         if (size == 0) {
             return new IMemento[0];
         }

         // Extract each node with given type.
 ArrayList list = new ArrayList (size);
         for (int nX = 0; nX < size; nX++) {
             Node node = nodes.item(nX);
             if (node instanceof Element ) {
                 Element element = (Element ) node;
                 if (element.getNodeName().equals(type)) {
                     list.add(element);
                 }
             }
         }

         // Create a memento for each node.
 size = list.size();
         IMemento[] results = new IMemento[size];
         for (int x = 0; x < size; x++) {
             results[x] = new XMLMemento(factory, (Element ) list.get(x));
         }
         return results;
     }

     /* (non-Javadoc)
      * Method declared in IMemento.
      */
     public Float getFloat(String key) {
         Attr attr = element.getAttributeNode(key);
         if (attr == null) {
             return null;
         }
         String strValue = attr.getValue();
         try {
             return new Float (strValue);
         } catch (NumberFormatException e) {
             WorkbenchPlugin.log("Memento problem - Invalid float for key: " //$NON-NLS-1$
 + key + " value: " + strValue, e); //$NON-NLS-1$
 return null;
         }
     }

     /* (non-Javadoc)
      * Method declared in IMemento.
      */
     public String getID() {
         return element.getAttribute(TAG_ID);
     }

     /* (non-Javadoc)
      * Method declared in IMemento.
      */
     public Integer getInteger(String key) {
         Attr attr = element.getAttributeNode(key);
         if (attr == null) {
             return null;
         }
         String strValue = attr.getValue();
         try {
             return new Integer (strValue);
         } catch (NumberFormatException e) {
             WorkbenchPlugin
                     .log("Memento problem - invalid integer for key: " + key //$NON-NLS-1$
 + " value: " + strValue, e); //$NON-NLS-1$
 return null;
         }
     }

     /* (non-Javadoc)
      * Method declared in IMemento.
      */
     public String getString(String key) {
         Attr attr = element.getAttributeNode(key);
         if (attr == null) {
             return null;
         }
         return attr.getValue();
     }

     /* (non-Javadoc)
      * Method declared in IMemento.
      */
     public String getTextData() {
         Text textNode = getTextNode();
         if (textNode != null) {
             return textNode.getData();
         }
         return null;
     }

     /**
      * Returns the Text node of the memento. Each memento is allowed only
      * one Text node.
      *
      * @return the Text node of the memento, or <code>null</code> if
      * the memento has no Text node.
      */
     private Text getTextNode() {
         // Get the nodes.
 NodeList nodes = element.getChildNodes();
         int size = nodes.getLength();
         if (size == 0) {
             return null;
         }
         for (int nX = 0; nX < size; nX++) {
             Node node = nodes.item(nX);
             if (node instanceof Text ) {
                 return (Text ) node;
             }
         }
         // a Text node was not found
 return null;
     }

     /**
      * Places the element's attributes into the document.
      * @param copyText true if the first text node should be copied
      */
     private void putElement(Element element, boolean copyText) {
         NamedNodeMap nodeMap = element.getAttributes();
         int size = nodeMap.getLength();
         for (int i = 0; i < size; i++) {
             Attr attr = (Attr ) nodeMap.item(i);
             putString(attr.getName(), attr.getValue());
         }

         NodeList nodes = element.getChildNodes();
         size = nodes.getLength();
         // Copy first text node (fixes bug 113659).
 // Note that text data will be added as the first child (see putTextData)
 boolean needToCopyText = copyText;
         for (int i = 0; i < size; i++) {
             Node node = nodes.item(i);
             if (node instanceof Element ) {
                 XMLMemento child = (XMLMemento) createChild(node.getNodeName());
                 child.putElement((Element ) node, true);
             } else if (node instanceof Text && needToCopyText) {
                 putTextData(((Text ) node).getData());
                 needToCopyText = false;
             }
         }
     }

     /* (non-Javadoc)
      * Method declared in IMemento.
      */
     public void putFloat(String key, float f) {
         element.setAttribute(key, String.valueOf(f));
     }

     /* (non-Javadoc)
      * Method declared in IMemento.
      */
     public void putInteger(String key, int n) {
         element.setAttribute(key, String.valueOf(n));
     }

     /* (non-Javadoc)
      * Method declared in IMemento.
      */
     public void putMemento(IMemento memento) {
         // Do not copy the element's top level text node (this would overwrite the existing text).
 // Text nodes of children are copied.
 putElement(((XMLMemento) memento).element, false);
     }

     /* (non-Javadoc)
      * Method declared in IMemento.
      */
     public void putString(String key, String value) {
         if (value == null) {
             return;
         }
         element.setAttribute(key, value);
     }

     /* (non-Javadoc)
      * Method declared in IMemento.
      */
     public void putTextData(String data) {
         Text textNode = getTextNode();
         if (textNode == null) {
             textNode = factory.createTextNode(data);
             // Always add the text node as the first child (fixes bug 93718)
 element.insertBefore(textNode, element.getFirstChild());
         } else {
             textNode.setData(data);
         }
     }

     /**
      * Saves this memento's document current values to the
      * specified writer.
      *
      * @param writer the writer used to save the memento's document
      * @throws IOException if there is a problem serializing the document to the stream.
      */
     public void save(Writer writer) throws IOException {
         DOMWriter out = new DOMWriter(writer);
         try {
             out.print(element);
         } finally {
             out.close();
         }
     }

     /**
      * A simple XML writer. Using this instead of the javax.xml.transform classes allows
      * compilation against JCL Foundation (bug 80053).
      */
     private static final class DOMWriter extends PrintWriter {
         
         private int tab;

         /* constants */
         private static final String XML_VERSION = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"; //$NON-NLS-1$

         /**
          * Creates a new DOM writer on the given output writer.
          *
          * @param output the output writer
          */
         public DOMWriter(Writer output) {
             super(output);
             tab = 0;
             println(XML_VERSION);
         }

         /**
          * Prints the given element.
          *
          * @param element the element to print
          */
         public void print(Element element) {
             // Ensure extra whitespace is not emitted next to a Text node,
 // as that will result in a situation where the restored text data is not the
 // same as the saved text data.
 boolean hasChildren = element.hasChildNodes();
             startTag(element, hasChildren);
             if (hasChildren) {
                 tab++;
                 boolean prevWasText = false;
                 NodeList children = element.getChildNodes();
                 for (int i = 0; i < children.getLength(); i++) {
                     Node node = children.item(i);
                     if (node instanceof Element ) {
                         if (!prevWasText) {
                             println();
                             printTabulation();
                         }
                         print((Element ) children.item(i));
                         prevWasText = false;
                     }
                     else if (node instanceof Text ) {
                         print(getEscaped(node.getNodeValue()));
                         prevWasText = true;
                     }
                 }
                 tab--;
                 if (!prevWasText) {
                     println();
                     printTabulation();
                 }
                 endTag(element);
             }
         }

         private void printTabulation() {
             // Indenting is disabled, as it can affect the result of getTextData().
 // In 3.0, elements were separated by a newline but not indented.
 // This causes getTextData() to return "\n" even if no text data had explicitly been set.
 // The code here emulates that behaviour.

 // for (int i = 0; i < tab; i++)
 // super.print("\t"); //$NON-NLS-1$
 }

         private void startTag(Element element, boolean hasChildren) {
             StringBuffer sb = new StringBuffer ();
             sb.append("<"); //$NON-NLS-1$
 sb.append(element.getTagName());
             NamedNodeMap attributes = element.getAttributes();
             for (int i = 0; i < attributes.getLength(); i++) {
                 Attr attribute = (Attr )attributes.item(i);
                 sb.append(" "); //$NON-NLS-1$
 sb.append(attribute.getName());
                 sb.append("=\""); //$NON-NLS-1$
 sb.append(getEscaped(String.valueOf(attribute.getValue())));
                 sb.append("\""); //$NON-NLS-1$
 }
             sb.append(hasChildren ? ">" : "/>"); //$NON-NLS-1$ //$NON-NLS-2$
 print(sb.toString());
         }

         private void endTag(Element element) {
             StringBuffer sb = new StringBuffer ();
             sb.append("</"); //$NON-NLS-1$
 sb.append(element.getNodeName());
             sb.append(">"); //$NON-NLS-1$
 print(sb.toString());
         }
         
         private static void appendEscapedChar(StringBuffer buffer, char c) {
             String replacement = getReplacement(c);
             if (replacement != null) {
                 buffer.append('&');
                 buffer.append(replacement);
                 buffer.append(';');
             } else {
                 buffer.append(c);
             }
         }

         private static String getEscaped(String s) {
             StringBuffer result = new StringBuffer (s.length() + 10);
             for (int i = 0; i < s.length(); ++i) {
                 appendEscapedChar(result, s.charAt(i));
             }
             return result.toString();
         }

         private static String getReplacement(char c) {
             // Encode special XML characters into the equivalent character references.
 // The first five are defined by default for all XML documents.
 // The next three (#xD, #xA, #x9) are encoded to avoid them
 // being converted to spaces on deserialization
 // (fixes bug 93720)
 switch (c) {
                 case '<' :
                     return "lt"; //$NON-NLS-1$
 case '>' :
                     return "gt"; //$NON-NLS-1$
 case '"' :
                     return "quot"; //$NON-NLS-1$
 case '\'' :
                     return "apos"; //$NON-NLS-1$
 case '&' :
                     return "amp"; //$NON-NLS-1$
 case '\r':
                     return "#x0D"; //$NON-NLS-1$
 case '\n':
                     return "#x0A"; //$NON-NLS-1$
 case '\u0009':
                     return "#x09"; //$NON-NLS-1$
 }
             return null;
         }
     }
 }

